﻿/*
 *   DateTimeFunctions.cs
 *   Copyright (c) 2011 - BitFox Software and Ricardo Federico Villafañes
 *   Contact: federvillaf@hotmail.com
 *   
 *   Licensing: This product is covered by Microsoft Reciprocal License (MS-RL)
 *              see https://bitfoxeval.codeplex.com/license
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics.CodeAnalysis;

namespace BitFox.Evaluator.Functions
{
    /// <summary>
    /// Built-in date-time functions for the evaluator.
    /// </summary>
    public static class DateTimeFunctions
    {
        /// <summary>
        /// TODO: This names should be resource'd
        /// </summary>
        static string[] dayNames = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };

        /// <summary>
        /// TODO: This names should be resource'd
        /// </summary>
        static string[] monthNames = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };
        
        /// <summary>
        /// The <b>D</b>ay <b>O</b>f <b>Week</b> function.
        /// </summary>
        /// <param name="date">The datetime for which the day of week will be returned.</param>
        /// <returns>1=Sunday,  ..., 7=Saturday</returns>
        static public int Dow(DateTime date) 
        {
            switch (date.DayOfWeek)
            {
                case DayOfWeek.Sunday: return 1;
                case DayOfWeek.Monday: return 2;
                case DayOfWeek.Tuesday: return 3;
                case DayOfWeek.Wednesday: return 4;
                case DayOfWeek.Thursday: return 5;
                case DayOfWeek.Friday: return 6;
                default: return 7;
            }
        }

        /// <summary>
        /// Returns the name of the day of week
        /// </summary>
        /// <param name="date">The datetime for wich the name of corresponding day of week will be returned</param>
        /// <returns>"Sunday", ..., "Saturday"</returns>
        static public string Cdow(DateTime date) 
        { 
            return dayNames[Dow(date) - 1]; 
        }

        /// <summary>
        /// Function that return the day of the passed datetime
        /// </summary>
        /// <param name="date">The datetime for which the day will be returned</param>
        /// <returns>The day of passed datetime, as an integer (1, ..., 31)</returns>
        static public int Day(DateTime date) 
        { 
            return date.Day; 
        }

        /// <summary>
        /// Function that return the month of the passed datetime
        /// </summary>
        /// <param name="date">The datetime for which the month will be returned</param>
        /// <returns>The month of passed datetime (1, ..., 12)</returns>
        static public int Month(DateTime date) 
        { 
            return date.Month; 
        }

        /// <summary>
        /// Function that return the year of the passed datetime
        /// </summary>
        /// <param name="date">The datetime for which the year will be returned</param>
        /// <returns>The year of passed datetime</returns>
        static public int Year(DateTime date) 
        { 
            return date.Year; 
        }

        /// <summary>
        /// Function that return the name of the month of passed datetime.
        /// </summary>
        /// <param name="date">The datetime for which the name of the month will be returned</param>
        /// <returns>"January", ..., "December"</returns>
        static public string Cmonth(DateTime date) 
        { 
            return monthNames[date.Month - 1]; 
        }

        /// <summary>
        /// Function that returnes the today's date.
        /// </summary>
        /// <returns>Only date part is returned, no info about hour, etc</returns>
        static public DateTime Date() 
        { 
            DateTime actual = DateTime.Now;
            return new DateTime(actual.Year, actual.Month, actual.Day);
        }

        /// <summary>
        /// Function that returns the number of seconds ellapsed from last midnight.
        /// </summary>
        /// <returns>The number of seconds. 'Milliseconds' are stored in the decimal part of result</returns>
        static public decimal Seconds() 
        {
            DateTime actual = DateTime.Now;
            return actual.Hour * 3600 + actual.Minute * 60 + actual.Second + actual.Millisecond / 1000;
        }

        /// <summary>
        /// The actual time (without date part)
        /// </summary>
        /// <returns>The actual time as a string: HH:MM:SS, alwasy 8 charactes length</returns>
        static public string Time() 
        {
            DateTime actual = DateTime.Now;
            return
                actual.Hour.FoxToString().PadLeft(2, '0') + ":" +
                actual.Minute.FoxToString().PadLeft(2, '0') + ":" +
                actual.Second.FoxToString().PadLeft(2, '0');
        }
        
        /// <summary>
        /// Converts the passed date to a string suitable for comparisons.
        /// </summary>
        /// <param name="actual">The date to be converted</param>
        /// <returns>The date, as a string with the following format: YYYYMMDD </returns>
        static public string Dtos(DateTime actual) 
        {
            return 
                (actual.Year * 10000 + actual.Month * 100 + actual.Day).FoxToString().PadLeft(8, '0');
        }

        /// <summary>
        /// This variation of DTOC returns a date converted to a string, suitable for comparisons
        /// </summary>
        /// <param name="actual">The date to be converted to a string</param>
        /// <param name="nMethod">A flag. It's only presence makes this kind of convertion. Its value is not important.</param>
        /// <returns>The date, as a string with the following format: YYYYMMDD </returns>
        [SuppressMessage("Microsoft.Usage", "CA1801")]
        static public string Dtoc(DateTime actual, int nMethod)
        {
            return Dtos(actual);
        }

        /// <summary>
        /// Return a datetime converted to a string.
        /// </summary>
        /// <param name="actual">The date that will be converted to a string</param>
        /// <param name="cEnv">The context under which the function is executed. This function adapt itself to the setting
        /// of <b>cEnv.SetCenturyOn</b> and <b>cEnv.SetDate</b></param>
        /// <returns>A string that represents the passed date. The format of this string is determined by the current setting
        /// of 'CalculatorEnvironment.CEnv.SetCenturyOn' and 'CalculatorEnvironment.CEnv.SetDate' The possible formats are:
        ///         SetCentury=On   SetCentury=Off
        ///         SetDate=Japan/Ansi/YMD or British/French/Italian/German/DMY or Usa/American/MDY
        /// </returns>
        static public string Dtoc(CalculatorContext cEnv, DateTime actual)
        {
            if (cEnv == null) throw new ArgumentNullException("cEnv");

            string xday = actual.Day.FoxToString().PadLeft(2, '0');
            string xmonth = actual.Month.FoxToString().PadLeft(2, '0');
            string xyear = actual.Year.FoxToString().PadLeft(4, '0');
            if (!cEnv.SetCenturyOn)
                xyear = xyear.Substring(2);

            switch (cEnv.SetDate)
            {
                case SetDateOptions.Ansi:
                    return xyear + "." + xmonth + "." + xday;

                case SetDateOptions.British:
                case SetDateOptions.French:
                case SetDateOptions.DMY:
                    return xday + "/" + xmonth + "/" + xyear;

                case SetDateOptions.German:
                    return xday + "." + xmonth + "." + xyear;

                case SetDateOptions.Italian:
                    return xday + "-" + xmonth + "-" + xyear;

                case SetDateOptions.Japan:
                case SetDateOptions.YMD:
                    return xyear + "/" + xmonth + "/" + xday;


                case SetDateOptions.Usa:
                    return xmonth + "-" + xday + "-" + xyear;

                case SetDateOptions.American:
                case SetDateOptions.MDY:
                default:
                    return xmonth + "/" + xday + "/" + xyear;
            }
        }

        /// <summary>
        /// Function that convert the passed date in a string with the format of 'dd' 'month-name' 'year'
        /// </summary>
        /// <param name="actual">The date to convert</param>
        /// <param name="cEnv">The context under which the function is executed. This function adapt itself to the setting
        /// of <b>cEnv.SetCenturyOn</b> and <b>cEnv.SetDate</b></param>
        /// <returns>The passed date represented as a string</returns>
        static public string Dmy(CalculatorContext cEnv, DateTime actual) 
        {
            if (cEnv == null) throw new ArgumentNullException("cEnv");

            string xyear = actual.Year.FoxToString();
            if (!cEnv.SetCenturyOn)
                xyear = xyear.Substring(2);

            return
                actual.Day.FoxToString() + " " +
                monthNames[actual.Month - 1] + " " +
                xyear;
        }

        /// <summary>
        /// Function that convert the passed date in a string with the format of 'month-name' 'dd' 'year'
        /// </summary>
        /// <param name="actual">The date too convert</param>
        /// <param name="cEnv">The context under which the function is executed. This function adapt itself to the setting
        /// of <b>cEnv.SetCenturyOn</b> and <b>cEnv.SetDate</b></param>
        /// <returns>The passed date represented as a string</returns>
        static public string Mdy(CalculatorContext cEnv, DateTime actual) 
        {
            if (cEnv == null) throw new ArgumentNullException("cEnv");
            
            string xyear = actual.Year.FoxToString();
            if (!cEnv.SetCenturyOn)
                xyear = xyear.Substring(2);

            return
                monthNames[actual.Month - 1] + " " +
                actual.Day.FoxToString() + ", " +
                xyear;
        }

        /// <summary>
        /// Function that parse a string and convert it into a date constante
        /// </summary>
        /// <param name="sDate">The string to parse. Following rules apply:
        /// - If constant start with {^ then you are passing a 'strict' constant with year/month/day in that order.
        /// - Else, the parameters are parsed as year or month or date depending upon the SET DATE flag.
        /// </param>
        /// <param name="cEnv">The context under which the function is executed. This function adapt itself to the setting
        /// of <b>cEnv.SetCenturyOn</b> and <b>cEnv.SetDate</b></param>
        /// <returns>The parsed constant</returns>
        static public DateTime Ctod(CalculatorContext cEnv, string sDate)
        {
            if (cEnv == null) throw new ArgumentNullException("cEnv");

            int ptr = 0;
            return ParseDateConstant(cEnv, sDate, ref ptr);
        }

        /// <summary>
        /// Helper method that do the parsing of a date-constant. It's utilized in preceding CTOD function and in evaluator,
        /// when a date-constant is parsed from expression stream.
        /// </summary>
        /// <param name="cEnv">The context under which the function is executed. This function adapt itself to the setting
        /// of <b>cEnv.SetCenturyOn</b> and <b>cEnv.SetDate</b></param>
        /// <param name="src">The source that contains, with other things, the date-constant to parse</param>
        /// <param name="srcPtr">The position in 'src' where the date-constant starts</param>
        /// <returns>The parsed date constant.</returns>
        [SuppressMessage("Microsoft.Maintainability", "CA1502")]
        static internal DateTime ParseDateConstant(CalculatorContext cEnv, string src, ref int srcPtr)
        {
            if (cEnv == null) throw new ArgumentNullException("cEnv");

            bool strict = false;
            if (src[srcPtr] == '^')
            {
                strict = true;
                srcPtr++;
            } 

            // Empty dates
            while (char.IsWhiteSpace(src[srcPtr]))
                srcPtr++;
            if (src[srcPtr] == '}')
                return DateTime.MaxValue;

            // Do the scan on input
            int p1 = -1, p2 = -1, p3 = -1;
            int part = 0;
            int state = 0;  // 0=Skipping blanks, 1=Waiting separator, 2=Processing digits
            while (srcPtr < src.Length && p3 < 0)
            {
                char ch = src[srcPtr++];
                switch (ch)
                {
                    case '0':
                    case '1':
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9':
                        if (state == 1) return DateTime.MinValue;
                        state = 2;
                        part = part * 10 + ((byte)ch) - 48;
                        break;

                    case ' ':
                    case '\t':
                        if (state == 2) state = 1;
                        break;

                    case '/':
                    case '-':
                    case '.':
                        state = 0;
                        if (p1 < 0) p1 = part;
                        else if (p2 < 0) p2 = part;
                        else if (p3 < 0) p3 = part;
                        part = 0;
                        break;

                    default:
                        srcPtr--;
                        if (p3 < 0) p3 = part;
                        break;
                }
            }

            // Perform date composition
            if (p1 == 0 && p2 == 0) return DateTime.MaxValue;
            if (p3 < 0 && part > 0) p3 = part;
            if (p3 < 0) return DateTime.MinValue;
            try
            {
                if (strict) return new DateTime(p1, p2, p3);
                switch (cEnv.SetDate)
                {
                    case SetDateOptions.Japan:
                    case SetDateOptions.YMD:
                    case SetDateOptions.Ansi:
                        return new DateTime(p1, p2, p3);            // Y M D

                    case SetDateOptions.Italian:
                    case SetDateOptions.German:
                    case SetDateOptions.British:
                    case SetDateOptions.French:
                    case SetDateOptions.DMY:
                        return new DateTime(p3, p2, p1);            // D M Y

                    case SetDateOptions.Usa:
                    case SetDateOptions.American:
                    case SetDateOptions.MDY:
                    default:
                        return new DateTime(p3, p1, p2);            // M D Y
                }
            }
            catch (ArgumentException) { return DateTime.MinValue; }
        }

        /// <summary>
        /// Function that adds an specified number of months to passed date.
        /// </summary>
        /// <param name="date">The date to which to add/substract months.</param>
        /// <param name="addMonth">The number of months to add/substract</param>
        /// <returns>The transformed date</returns>
        static public DateTime GoMonth(DateTime date, int addMonth) 
        {
            if (addMonth == 0) return date;
            return date.AddMonths(addMonth);
        }
    }
}
